// ignore_for_file: constant_identifier_names

import 'dart:io';

import 'package:dart_bean/config/config.dart';
import 'package:dart_bean/feature/auto_json_bean.dart';
import 'package:dart_bean/cass_info.dart';
import 'package:dart_bean/json_to_bean/util.dart';
import 'package:dart_bean/setting.dart';
import 'package:dart_bean/template.dart';
import 'package:dart_bean/util/clipboard.dart';
import 'package:dart_bean/util/dart_source_updater.dart';
import 'package:dart_bean/util/part_generator.dart';
import 'package:dart_bean/util/shell_arg.dart';
import 'package:yaml/yaml.dart';

import 'util/ext.dart';

final Set<String> _errors = {};

void onError(String msg) {
  _errors.add(msg);
}

class Generator {
  static bool testMode = false;
  static late Config config;
  static late String projectName;
  static late Directory libDir, genDir;

  ///import 'package:xx/xxx.dart';
  static bool packageStyle = false;

  final Directory projectDir;
  final List<CassInfo> classList = [];
  Directory? searchBeanDir;

  Generator(this.projectDir, Directory? libDir, this.searchBeanDir) {
    _resetStateForTest();
    _init(libDir);
  }

  static Future<void> startMain(List<String> args) async {
    final arg = ShellArg.parse(args);
    arg.checkArgs();
    var projectDir = arg.projectDir!;

    final libDir = Directory("$projectDir/${arg.libDir}".fixePathStyle());

    Directory? searchDir;
    if (arg.searchDir != null) {
      searchDir = Directory("$projectDir/${arg.searchDir}".fixePathStyle());
    }
    config = parseConfig(Directory(projectDir));
    packageStyle = arg.libDir == 'lib';

    final generator = Generator(Directory(projectDir), libDir, searchDir);
    if (arg.json2bean) startJsonToBean(arg);
    await generator.gen();
    _throwAllErrorOnEnd();
  }

  static void startJsonToBean(ShellArg arg) {
    var beanName = arg.beanName!;
    final output = File('${libDir.path}/$beanName.dart'.fixePathStyle());
    beanName = beanName.substringAfterLast('/');
    beanName = beanName.split('_').map((e) => e.upperFirst()).join();
    final jsonString = getClipboardContent();
    jsonToBean(jsonString, output, beanName);
  }

  static void _throwAllErrorOnEnd() {
    if (_errors.isNotEmpty) {
      final sb = StringBuffer();
      sb.writeln('********************** error list ****************************');
      for (var e in _errors) {
        sb.writeln(e);
        sb.writeln('**************************************************************');
      }
      if (!testMode) {
        stderr.writeln(sb.toString());
        exitCode = 1;
      } else {
        throw sb.toString();
      }
    }
  }

  static final Map<File, GenCodeMaterial> _genCodeMaterials = {};

  static GenCodeMaterial getGenPartMaterial(File targetClassDartFile) {
    var m = _genCodeMaterials[targetClassDartFile];
    if (m == null) _genCodeMaterials[targetClassDartFile] = m = GenCodeMaterial();
    return m;
  }

  void _init(Directory? libDir) {
    Generator.libDir = libDir ?? Directory('${projectDir.path}${separator}lib');
    genDir = Directory('${Generator.libDir.path}$separator$genDirName');
  }

  void _resetStateForTest() {
    _errors.clear();
  }

  Future<void> gen() async {
    projectName = await parseProjectName(projectDir);
    if (genDir.existsSync()) genDir.deleteSync(recursive: true); //delete all gen files
    genBaseFiles(libDir);
    final genPath = genDir.path;
    final files = (searchBeanDir ?? libDir).listSync(recursive: true);
    Map<File, List<CassInfo>> tasks = {};
    for (var f in files) {
      final path = f.path;
      if (!path.startsWith(genPath) && !path.endsWith('.g.dart') && path.endsWith('.dart')) {
        final file = File(f.path);
        final singleFileClassList = CassInfo.parseFileToClasses(libDir, file);
        if (singleFileClassList.isNotEmpty) {
          tasks[file] = singleFileClassList;
          classList.addAll(singleFileClassList);
        }
      }
    }
    tasks.forEach((file, singleFileClassList) {
      _genSingleFileCode(file, singleFileClassList);
    });
  }

  void _genSingleFileCode(File file, Iterable<CassInfo> classList) {
    print('genCode for ${printFilePath(file)}');
    PartGenerator.start(classList);

    final dartSourceUpdater = DartSourceUpdater(file);
    AutoJsonBean(dartSourceUpdater, classList).update();
    dartSourceUpdater.commitUpdates();
  }

  void genBaseFiles(Directory libDir) {
    _genBaseFile(jsonSupportFileName, templateJsonSupport);
    var template = jsonCastTemplateApplySetting(autoDefaultValueStyle ? templateJsonCast : templateJsonCastDisableAutoDefaultValue);
    _genBaseFile(jsonCastFileName, template);
  }

  String jsonCastTemplateApplySetting(String template) {
    var reg = RegExp(r'JSONConvert [$\w]+ = \$DefaultJSONConvert\(\);');
    template = template.replaceFirst(reg, 'JSONConvert $jsonCastParamName = \$DefaultJSONConvert();');
    reg = RegExp(r'K\? [$\w]+<K extends Enum, V>');
    template = template.replaceFirst(reg, 'K? $decodeEnumName<K extends Enum, V>');
    return template;
  }

  void _genBaseFile(String fileName, String content) {
    final file = getBaseJsonFile(genDir, fileName);
    ensureParent(file);
    file.writeAsStringSync(safeDartFormatter(content));
  }
}

Future<String> parseProjectName(Directory projectDir) async {
  final fileName = 'pubspec.yaml';
  try {
    final file = File('${projectDir.path}/$fileName');
    final doc = loadYaml(File(file.path).readAsStringSync());
    final name = doc['name'];
    if (name == null) throw 'ProjectName not found in ${file.path}';
    return name;
  } catch (e) {
    rethrow;
    // throw 'no $fileName found to find ProjectName, in path: ${projectDir.path}';
  }
}

String genImportPath(File dartFile, File into) {
  if (Generator.packageStyle) {
    final path = getRelativePath(dartFile, Generator.libDir);
    return 'package:${Generator.projectName}/$path';
  } else {
    //relative style
    final target = getRelativePath(dartFile, Generator.libDir);
    var to = getRelativePath(into, Generator.libDir);
    var end = to.lastIndexOf('/');
    if (end != -1) {
      to = to.substring(0, end);
      to = to.replaceAll(RegExp(r'[^/]+'), '#').replaceAll('#', '..');
      return '$to/$target';
    } else {
      return target;
    }
  }
}
